Avoid deadlock during first time initialisation #657
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Since the
UserDefaults
calls made bysaveIdentity
cause blockingNSNotifications
to be emitted to the main thread, callingsaveIdentity
under the write lock can cause deadlocks.Since the
saveIdentity
call only reads state it’s now moved to only take a read lock instead of being under the write lock.The problem
In my Mac app I initialise Mixpanel on a background thread to avoid blocking the main thread during launch. This works fine for most users, however when the app loses focusses immediately during the very first launch Mixpanel deadlocks. Freezing the app completely. (This happens most often when users install the app through the App Store en then click the "Open" button in the App Store itself, this launches the app, but als takes back the focus immediately.)
Attached here you can find the stacktrace where the app hangs: MixpanelHangStackTrace.txt
The cause
Turns out that the
MixpanelInstance.unarchive
method callsMixpanelPersistence.saveIdentity
while insideReadWriteLock.write
. This in turn writes toUserDefaults
which blocks until it'sNSNotification
for the defaults updates are picked up by the runloop on the main thread.However at the moment that unarchive is doing its thing, the main thread is busy handing the
NSApplication.willResignActiveNotification
this wants to check the opt-out state and therefore is waiting for theReadWriteLock
to become available again. Causing a deadlock.Solution
Since strictly all state writing is already done by the time
saveIdentity
is called, I think the most robust solution is to move thesaveIdentity
out of the write lock. It does need a read lock, because it does read the internal state to store it inUserDefaults
, but writing isn't needed. CallingsaveIdentity
is also done without a read lock in other places in the codebase.I made the necessary adjustments in this PR and verified this solves the deadlock in my case.
Note on reproduction
I can quite easily reproduce this in my own app by putting a breakpoint in the
unarchive
method on the line setting theanonymousId
. Hitting the breakpoint makes my app resign focus (because Xcode takes focus) while still under the write lock.However when I tried to make a reproduction sample I found it hard to make a simple demo. Since it only happens at first launch and I need the breakpoint to trigger it reliably.